package com.herionthenet;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public abstract class SocketClient extends SocketClientAdapter {
	
	private String host;
	private int port;
	private FrameDecoder frameDecoder;
	private FrameEncoder frameEncoder;
	private SocketChannel client;
	private Selector selector;
	private Thread thread;
	
	private SocketClientHandler handler;
	private boolean isConnected;
	
	public SocketClient(String host, int port, FrameDecoder decoder, FrameEncoder encoder){
		if(host == null || port == 0)
			throw new IllegalArgumentException("Invalid address for " + host + ":" + port);
		
		this.host = host;
		this.port = port;
		this.frameDecoder = decoder;
		this.frameEncoder = encoder;		
	}
	
	public void connect() {
		if( thread != null )
			throw new IllegalStateException("already/still connected");
		thread = new Thread(this);
		thread.setName(SocketClient.class.getName());
		thread.start();
	}
		
	public void send(Object object) throws InterruptedException, NotYetConnectedException{		
		if(handler != null)
			handler.send(object);
	}
	
	@Override
	public void run() {	
		if(thread == null)
			thread = Thread.currentThread();
		
		interruptableRun();
		thread = null;
	}

	protected final void interruptableRun() {
		try {
			tryToConnect(new InetSocketAddress(this.host, this.port));
		} catch (IOException e) {			
			onError(e);
		}
		
		handler = new SocketClientHandler(this, client);
		
		try {
			
			while(!handler.isClosed()){
				if(Thread.interrupted())
					handler.close();
				
				SelectionKey key = null;
				handler.flush();
				selector.select();
				
				Set<SelectionKey> keys = selector.selectedKeys();
				Iterator<SelectionKey> i = keys.iterator();
				
				while(i.hasNext()){
					key = i.next();
					i.remove();
					
					if(!key.isValid())
						continue;
					
					if(key.isConnectable()){
						finnishConnect();
					}
					
					if(key.isReadable())
						handler.handleRead();
					
					if(key.isWritable())
						handler.flush();
				}
			}
			
		} catch (IOException e) {
			onError(e);
			handler.close();
		}
		
		try {
			selector.close();
		} catch (IOException e) {			
			e.printStackTrace();
		}
		
		selector = null;
		
		try {
			client.close();
		} catch (IOException e) {			
			e.printStackTrace();
		}
		
		client = null;
		
	}	

	private void tryToConnect(InetSocketAddress remote) throws IOException{
		client = SocketChannel.open();
		client.configureBlocking(false);
		client.connect(remote);
		
		selector = Selector.open();
		client.register(selector, SelectionKey.OP_CONNECT);
	}
	
	private void finnishConnect() throws IOException {
		if(client.isConnectionPending())
			client.finishConnect();
		
		onConnected();
		isConnected = true;
		
		// Interest in READ mode, after connected to server
		client.register(selector, SelectionKey.OP_READ);
	}

	@Override
	public final void onWriteDemmand() {
		selector.wakeup();
	}
	
	@Override
	public boolean isConnected(){
		return isConnected;
	}
	
	@Override
	public void onMessage(byte[] bytes) {
	};
	
	@Override
	public FrameDecoder getFrameDecoder() {	
		return frameDecoder;
	}
	
	@Override
	public FrameEncoder getFrameEncoder() {	
		return frameEncoder;
	}
}
